Skip to main content

SwiftData

·1246 words·6 mins

Now that I’ve upgraded my computer I can use the latest updates in Swift, SwiftUI and the all-new SwiftData introduced last June. I decided to take an old project and update it to use SwiftData to see how it works.

Back in 2016 I made a companion app for an online role-playing game called Clan Lord that fetches the XML feed from the game server, parses it and displays it in an iOS app, along with some astronomical in-game data. I decided to use this real-world example that includes fetching data from the web to test SwiftData.

A few details about the app #

So before I go into what I changed to use SwiftData, a few word about what the app does. Every 3 minutes, the server for the game posts an XML feed with various data about the game. The app fetches this XML data and then parses into this ClanLordStatus struct:

11
12
13
14
15
16
17
struct ClanLordStatus: Codable {
    var status: Status
    var news: String
    var clanners: [OnlineExile]
    var announcements: [Announcement]
    var poptrend: [Popcount]
}

The status gives the date and time of the last refresh of the XML feed. The news always points to the latest release notes for server updates: Clan Lord what’s new, clanners contains the list of currently playing characters, each with the information detailed below, there are four types of announcements which are also detailed below, and finally poptrend is the number of online clanners for each update of the XML for the past few hours, which gives a population trend.

OnlineExile consist of these parameters:

24
25
26
27
28
29
30
31
32
33
 struct OnlineExile: Codable, Identifiable, Hashable {
     var name: String
     var started: Date
     var race: String
     var gender: String
     var profession: String
     var clan: String
     var picture: Int
     var colors: String
}

With this the app can display this type of information:

Image of the 'clanning' tab of the application.

And Announcement has these parameters:

67
68
69
70
71
72
73
struct Announcement: Codable, Identifiable {
    var type: String
    var time: Date
    var messenger: String
    var message: String
    var awardee: String
}

And the corresponding view within the app:

Image of the 'news' tab of the application.

In addition to these two views, the app also displays various information like the current time, the next sunrise and sunset, etc. Here is that view 1.

Image of the 'astro' tab of the application.

Adding SwiftData #

The information from the XML feed is ephemeral. People come and go, so the list of online exiles varies over time and there is nothing to persist. To test SwiftData I needed some data that would be persisted, so I decided to include the ability to add notes about an exile and also to enable notifications (to get notified when they log in the game).

My first thought was to simply use my old networking code to pull the XML and parse it into a ClanLordStatus struct. I’d have an ephemeral list of OnlineExile and a persistent list of Exile which contains the same information2 plus the notification and notes as well as an online parameter. When I refresh the XML feed I go through the list and update the online status and the app displays exiles that are online.

There are four steps to use SwiftData in an app:

  1. Add the @Model macro to the model class (it replaces @Observable).
  2. Pass in the ModelContainer to the view (best done from the @main struct so that all your views have access to the model but it’s not necessary).
  3. Add data to the model with .insert(modelObject).
  4. Read the data that you need for the purpose of the view with @Query.

So in my project I created the Exile model which is similar to the OnlineExile struct but adds the online, notify and notes parameters, and added the @Model macro like this. <- step 1

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Model
class Exile {
    @Attribute(.unique)
    var name: String
    var profession: String
    var race: String
    var gender: String
    var clan: String
    var started: Date = Date()
    var picture: Int = 0
    var colors: String = ""
    var online: Bool = false
    var notify: Bool = false
    var notes: String
}

Then I pass a ModelContainer to the ContentView (don’t forget to import SwiftData where needed). <- step 2

 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import SwiftData
import SwiftUI

@main
struct CL_InformerApp: App {
    @State private var networking = Networking()
    @State private var astro = AstroVM()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(networking)
                .environment(astro)
        }
        .modelContainer(for: Exile.self)
    }
}

To add data you need to have access to the ModelContext which represents your model in memory and is managed by SwiftData.

You get access to the ModelContext like this:

@Environment(\.modelContext) var modelContext

Then you add data simply like this. <- step 3

let newPerson = Exile(name: clanner.name, profession: profession, race: race, gender: clanner.gender, clan: clanner.clan)
modelContext.insert(newPerson)

And then when you need to access the data from the model all you need is to read the data with a query which can be configured with predicates and sorting options. So something like this for example. <- step 4

@Query private var people: [Exile]
...
NavigationStack {
    List {
        ForEach(people) { person in
            NavigationLink(value: person) {
                Text(person.name)
            }
        }
    }
    ...
}

That’s all it takes to use SwiftData! This is what the view for an ’exile’ looks like 3:

Image of the detailed view for an exile, showing the data from the server as well as a switch to enable notifications and a textfield to enter notes.

Current status #

So far so good! Of course, there’s a lot more to SwiftData than just that. This is only a first order implementation to get things working and it works pretty well as far as displaying who is online and persisting the notes. I’m also quite happy with the way the app looks (both in light and dark mode) and how the astronomical information is presented.

The issues I have is that sometimes the app isn’t very responsive, and more importantly the notifications don’t work. I think the performance issues can be resolved by refactoring the code to use async tasks so that changes to the model occur more smoothly. For notifications, I think I need to look into ModelActor, and that’ll be the subject of another post. Stay tuned!


  1. IRL means In Real Life, as opposed to CL for the Clan Lord gameworld. When it’s 3:30 PM in CL time, it’s currently 11:19 PM in real life. But it won’t be the case tomorrow. Times flows 4 times faster in the game world than in real life, so it’s always out of sync from ours and without tools like this it’s impossible to know the exact in-game time. ↩︎

  2. With the exception of the name of the character, most of the other parameters can change over time. People can change clan for example, and even the race and gender may change, because they can choose to be “undisclosed” at first and later on reveal it. And the profession can also change over time. So all these parameters will get updated with the new server data as they are fetched. ↩︎

  3. For now this is the information displayed, in the future I might add the avatar for the character. The information is contained in the picture parameter which gives an ID and colors parameter. This is included as part of the XML information. I just need to figure out how to get the picture from the CL Images file and then how to swap the specific exile’s colors with the placeholders in the image. ↩︎